{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pinecone-io/examples/blob/master/learn/search/faiss-ebook/locality-sensitive-hashing-random-projection/random_projection.ipynb) [![Open nbviewer](https://raw.githubusercontent.com/pinecone-io/examples/master/assets/nbviewer-shield.svg)](https://nbviewer.org/github/pinecone-io/examples/blob/master/learn/search/faiss-ebook/locality-sensitive-hashing-random-projection/random_projection.ipynb)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Random Projection\n",
    "\n",
    "First we read our data - we will use the **Sift1M** dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from sklearn.metrics.pairwise import cosine_similarity\n",
    "import pandas as pd\n",
    "\n",
    "# now define a function to read the fvecs file format of Sift1M dataset\n",
    "def read_fvecs(fp):\n",
    "    a = np.fromfile(fp, dtype='int32')\n",
    "    d = a[0]\n",
    "    return a.reshape(-1, d + 1)[:, 1:].copy().view('float32')\n",
    "\n",
    "# 1M samples\n",
    "wb = read_fvecs('../../../data/sift/sift_base.fvecs')\n",
    "# queries\n",
    "xq = read_fvecs('../../../data/sift/sift_query.fvecs')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can build our random projection hashing function like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def all_binary(n):\n",
    "    total = 1 << n\n",
    "    print(f\"{total} possible combinations\")\n",
    "    combinations = []\n",
    "    for i in range(total):\n",
    "        # get binary representation of integer\n",
    "        b = bin(i)[2:]\n",
    "        # pad zeros to start of binary representtion\n",
    "        b = '0' * (n - len(b)) + b\n",
    "        b = [int(i) for i in b]\n",
    "        combinations.append(b)\n",
    "    return combinations\n",
    "\n",
    "class RandomProjection:\n",
    "    # initialize what will be the buckets\n",
    "    buckets = {}\n",
    "    # initialize counter\n",
    "    counter = 0\n",
    "\n",
    "    def __init__(self, nbits, d):\n",
    "        self.nbits = nbits\n",
    "        self.d = d\n",
    "        # create our hyperplane normal vecs for splitting data\n",
    "        self.plane_norms = np.random.rand(d, nbits) - .5\n",
    "        print(f\"Initialized {self.plane_norms.shape[1]} hyperplane normal vectors.\")\n",
    "        # add every possible combination to hashes attribute as numpy array\n",
    "        self.hashes = all_binary(nbits)\n",
    "        # and add each as a key to the buckets dictionary\n",
    "        for hash_code in self.hashes:\n",
    "            # convert to string\n",
    "            hash_code = ''.join([str(i) for i in hash_code])\n",
    "            self.buckets[hash_code] = []\n",
    "        # convert self.hashes to numpy array\n",
    "        self.hashes = np.stack(self.hashes)\n",
    "\n",
    "    def get_binary(self, vec):\n",
    "        # calculate nbits dot product values\n",
    "        direction = np.dot(vec, projection.plane_norms)\n",
    "        # find positive direction (>0) and negative direction (<=0)\n",
    "        direction = direction > 0\n",
    "        # convert boolean array to integer strings\n",
    "        binary_hash = direction.astype(int)\n",
    "        return binary_hash\n",
    "        \n",
    "    def hash_vec(self, vec, show=False):\n",
    "        # generate hash\n",
    "        binary_hash = self.get_binary(vec)\n",
    "        # convert to string format for dictionary\n",
    "        binary_hash = ''.join(binary_hash.astype(str))\n",
    "        # add ID to buckets dictionary\n",
    "        self.buckets[binary_hash].append(self.counter)\n",
    "        if show:\n",
    "            print(f\"{self.counter}: {''.join(binary_hash)}\")\n",
    "        # increment counter\n",
    "        self.counter += 1\n",
    "    \n",
    "    def hamming(self, hashed_vec):\n",
    "        # get hamming distance between query vec and all buckets in self.hashes\n",
    "        hamming_dist = np.count_nonzero(hashed_vec != projection.hashes, axis=1).reshape(-1, 1)\n",
    "        # add hash values to each row\n",
    "        hamming_dist = np.concatenate((projection.hashes, hamming_dist), axis=1)\n",
    "        # sort based on distance\n",
    "        hamming_dist = hamming_dist[hamming_dist[:, -1].argsort()]\n",
    "        return hamming_dist\n",
    "    \n",
    "    def top_k(self, vec, k=5):\n",
    "        # generate hash\n",
    "        binary_hash = self.get_binary(vec)\n",
    "        # calculate hamming distance between all vectors\n",
    "        hamming_dist = self.hamming(binary_hash)\n",
    "        # loop through each bucket until we have k or more vector IDs\n",
    "        vec_ids = []\n",
    "        for row in hamming_dist:\n",
    "            str_hash = ''.join(row[:-1].astype(str))\n",
    "            bucket_ids = self.buckets[str_hash]\n",
    "            vec_ids.extend(bucket_ids)\n",
    "            if len(vec_ids) >= k:\n",
    "                vec_ids = vec_ids[:k]\n",
    "                break\n",
    "        # return top k IDs\n",
    "        return vec_ids"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(128, 12)"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "projection.plane_norms.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initialized 4 hyperplane normal vectors.\n",
      "16 possible combinations\n"
     ]
    }
   ],
   "source": [
    "projection = RandomProjection(4, wb.shape[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0, 0, 0, 0],\n",
       "       [0, 0, 0, 1],\n",
       "       [0, 0, 1, 0],\n",
       "       [0, 0, 1, 1],\n",
       "       [0, 1, 0, 0],\n",
       "       [0, 1, 0, 1],\n",
       "       [0, 1, 1, 0],\n",
       "       [0, 1, 1, 1],\n",
       "       [1, 0, 0, 0],\n",
       "       [1, 0, 0, 1],\n",
       "       [1, 0, 1, 0],\n",
       "       [1, 0, 1, 1],\n",
       "       [1, 1, 0, 0],\n",
       "       [1, 1, 0, 1],\n",
       "       [1, 1, 1, 0],\n",
       "       [1, 1, 1, 1]])"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "projection.hashes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have `16` groups that our vectors can be bucketed into. We run the bucketing process using the `hash_vec` method of `RandomProjection`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0: 0100\n"
     ]
    }
   ],
   "source": [
    "projection.hash_vec(wb[0], show=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we check the `projection.buckets` - where we have all all of our hash to ID mappings - we will find the same hash `0001` mapped to the value `0` - this is our *vector ID*, which we will simply use to keep track of our vectors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'0000': [],\n",
       " '0001': [],\n",
       " '0010': [],\n",
       " '0011': [],\n",
       " '0100': [0],\n",
       " '0101': [],\n",
       " '0110': [],\n",
       " '0111': [],\n",
       " '1000': [],\n",
       " '1001': [],\n",
       " '1010': [],\n",
       " '1011': [],\n",
       " '1100': [],\n",
       " '1101': [],\n",
       " '1110': [],\n",
       " '1111': []}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "projection.buckets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's continue bucketing all of the vectors in `wb` - this will take a few seconds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(1, len(wb)-1):\n",
    "    projection.hash_vec(wb[i])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let us have another look at the beginning of our `projection.buckets` - we will find that there are now many more vector IDs indexed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'0000': [1,\n",
       "  3,\n",
       "  6,\n",
       "  7,\n",
       "  8,\n",
       "  ...\n",
       "  504284,\n",
       "  505108,\n",
       "  505360,\n",
       "  505670,\n",
       "  ...]}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "projection.buckets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By adding each vector ID, this allows us to keep track of all rows (which we can map back to our original data) that have been grouped into each hash.\n",
    "\n",
    "Although inside our LSH index we **only** keep the lower resolution binary hashes, which we will later compare using Hamming distance. This can be problematic when we have *many* vectors being mapped to the same hash buckets - as we lose the ability to distinguish between each of those."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0000: 171125\n",
      "0001: 100654\n",
      "0010: 20754\n",
      "0011: 16737\n",
      "0100: 310361\n",
      "0101: 101092\n",
      "0110: 9079\n",
      "0111: 4126\n",
      "1000: 35584\n",
      "1001: 17225\n",
      "1010: 4493\n",
      "1011: 2306\n",
      "1100: 149024\n",
      "1101: 49430\n",
      "1110: 5952\n",
      "1111: 2057\n"
     ]
    }
   ],
   "source": [
    "for code in projection.hashes:\n",
    "    code_str = ''.join(code.astype(str))\n",
    "    print(f\"{code_str}: {len(projection.buckets[code_str])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we have many vectors mapped to each bucket. This is a problem but let's keep going and see what we return.\n",
    "\n",
    "Let's find the top `k` results for a single query vector from `xq`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[5, 13, 22, 26, 35, 37, 38, 39, 149, 152]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "top_10 = projection.top_k(xq[0], k=10)\n",
    "top_10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we already noted, our LSH index does **not** contain the original vectors, just the binary hashes. But we do have the ID values so we can map that back to our `wb` array to return the original vectors and calculate the original cosine similarity to see how accurate our LSH implementation is."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.50619876"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cos = cosine_similarity(wb[top_10], [xq[0]])\n",
    "np.mean(cos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's compare that to the cosine similarity from comparing all vectors in `wb`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.4223302"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cos = cosine_similarity(wb, [xq[0]])\n",
    "np.mean(cos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Okay, we get a slightly higher similarity from our top k vectors returned with our LSH implementation - however, this this could simply be beginners luck. So let's try a couple more queries, we'll write a function `sim_check` for running through the process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sim_check(query_vecs):\n",
    "    results = {'xq': [], 'wb': []}\n",
    "    for xq in query_vecs:\n",
    "        top_10 = projection.top_k(xq, k=10)\n",
    "        cos = cosine_similarity(wb[top_10], [xq])\n",
    "        cos = np.mean(cos)\n",
    "        results['xq'].append(cos)\n",
    "        cos = cosine_similarity(wb, [xq])\n",
    "        cos = np.mean(cos)    \n",
    "        results['wb'].append(cos)\n",
    "    print(f\"xq: {np.mean(results['xq'])}\")\n",
    "    print(f\"wb: {np.mean(results['wb'])}\")\n",
    "    return results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "xq: 0.5412288904190063\n",
      "wb: 0.43162330985069275\n"
     ]
    }
   ],
   "source": [
    "results = sim_check(xq[:50])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Okay awesome, so even after compressing our vectors of dimensionality `128` into **binary** vectors of dimensionality `4`, we have still retained enough information to produce slightly better than average results.\n",
    "\n",
    "*But*, they're not great. Within this dataset there are many high-similarity pairs with cosine similarity of ~0.8 - so returning an average of *0.53* is not exactly perfect.\n",
    "\n",
    "So what do we do? We have one parameter that we can modify, the `nbits` value. We increase the `nbits` value to increase the *resolution* of our hashing function, which means we will have smaller, more accurate buckets, and that leads search-quality to increase. *But* at the same time also means we have a larger index, more values to compare, and those values contain more dimensions. So tuning this parameter is a balancing act between increasing *search-quality* while maintaining a reasonable *search-speed*.\n",
    "\n",
    "Our performance is not great with an `nbits` value of `4`, let's increase that to an `nbits` value of `8`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0, 0, 0, 0],\n",
       "       [0, 0, 0, 1],\n",
       "       [0, 0, 1, 0],\n",
       "       [0, 0, 1, 1],\n",
       "       [0, 1, 0, 0],\n",
       "       [0, 1, 0, 1],\n",
       "       [0, 1, 1, 0],\n",
       "       [0, 1, 1, 1],\n",
       "       [1, 0, 0, 0],\n",
       "       [1, 0, 0, 1],\n",
       "       [1, 0, 1, 0],\n",
       "       [1, 0, 1, 1],\n",
       "       [1, 1, 0, 0],\n",
       "       [1, 1, 0, 1],\n",
       "       [1, 1, 1, 0],\n",
       "       [1, 1, 1, 1]])"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "projection.hashes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'1011'"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "''.join(projection.hashes[-5:][0].astype(str))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initialized 8 hyperplane normal vectors.\n",
      "256 possible combinations\n",
      "00000000: 247\n",
      "00000001: 4667\n",
      "00000010: 305\n",
      "00000011: 2489\n",
      "00000100: 2630\n"
     ]
    }
   ],
   "source": [
    "d = wb.shape[0]\n",
    "# initialize projection object\n",
    "projection = RandomProjection(8, wb.shape[1])\n",
    "# add all our vectors\n",
    "for i in range(len(wb)-1):\n",
    "    projection.hash_vec(wb[i])\n",
    "\n",
    "# we would expect generally smaller buckets\n",
    "for code in projection.hashes[:5]:\n",
    "    str_code = ''.join(code.astype(str))\n",
    "    print(f\"{str_code}: {len(projection.buckets[str_code])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we see that we have much smaller groups. Which means that we should find slightly better search-quality when returning `k` of the nearest neighbors. Let's rerun `sim_check` and see what we get. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "xq: 0.6719443798065186\n",
      "wb: 0.43162330985069275\n"
     ]
    }
   ],
   "source": [
    "results = sim_check(xq[:50])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Much better results, of-course, we could continue increasing `nbits` to get better and better results - however we would ideally want to do this with an optimized implementation of LSH - not what we are using here. In that case, we have many options - the `faiss` library includes an `IndexLSH` function which allows us to build an efficient LSH index which we can fine-tune using the `nbits` parameter as we have done here.\n",
    "\n",
    "For now, let's leave our implementation with a small visualization of our `check_sim` performance against multiple `nbits` values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".\n",
      "Initialized 2 hyperplane normal vectors.\n",
      "4 possible combinations\n",
      "xq: 0.5848360061645508\n",
      "wb: 0.43162330985069275\n",
      "Initialized 4 hyperplane normal vectors.\n",
      "16 possible combinations\n",
      "xq: 0.5784505009651184\n",
      "wb: 0.43162330985069275\n",
      "Initialized 8 hyperplane normal vectors.\n",
      "256 possible combinations\n",
      "xq: 0.6728818416595459\n",
      "wb: 0.43162330985069275\n",
      "Initialized 12 hyperplane normal vectors.\n",
      "4096 possible combinations\n",
      "xq: 0.7043259143829346\n",
      "wb: 0.43162330985069275\n",
      ".\n",
      "Initialized 2 hyperplane normal vectors.\n",
      "4 possible combinations\n",
      "xq: 0.530998706817627\n",
      "wb: 0.43162330985069275\n",
      "Initialized 4 hyperplane normal vectors.\n",
      "16 possible combinations\n",
      "xq: 0.6618399620056152\n",
      "wb: 0.43162330985069275\n",
      "Initialized 8 hyperplane normal vectors.\n",
      "256 possible combinations\n",
      "xq: 0.6009592413902283\n",
      "wb: 0.43162330985069275\n",
      "Initialized 12 hyperplane normal vectors.\n",
      "4096 possible combinations\n",
      "xq: 0.676262617111206\n",
      "wb: 0.43162330985069275\n",
      ".\n",
      "Initialized 2 hyperplane normal vectors.\n",
      "4 possible combinations\n",
      "xq: 0.5259867906570435\n",
      "wb: 0.43162330985069275\n",
      "Initialized 4 hyperplane normal vectors.\n",
      "16 possible combinations\n",
      "xq: 0.5397058725357056\n",
      "wb: 0.43162330985069275\n",
      "Initialized 8 hyperplane normal vectors.\n",
      "256 possible combinations\n",
      "xq: 0.668721616268158\n",
      "wb: 0.43162330985069275\n",
      "Initialized 12 hyperplane normal vectors.\n",
      "4096 possible combinations\n",
      "xq: 0.692796528339386\n",
      "wb: 0.43162330985069275\n"
     ]
    }
   ],
   "source": [
    "testing = pd.DataFrame({\n",
    "    'nbits': [],\n",
    "    'xq_sim': []\n",
    "})\n",
    "\n",
    "num_vecs = 50\n",
    "\n",
    "for epoch in range(3):\n",
    "    print('.')\n",
    "    for nbits in [2, 4, 8, 12]:\n",
    "        # initialize projection object\n",
    "        projection = RandomProjection(nbits, wb.shape[1])\n",
    "        # add all our vectors\n",
    "        for i in range(len(wb)-1):\n",
    "            projection.hash_vec(wb[i])\n",
    "        # get results from sim_check\n",
    "        results = sim_check(xq[:num_vecs])\n",
    "        testing = testing.append(pd.DataFrame({\n",
    "            'nbits': [nbits]*num_vecs,\n",
    "            'xq_sim': results['xq']\n",
    "        }), ignore_index=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<AxesSubplot:>"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "sns.lineplot(x=testing['nbits'].tolist(), y=testing['xq_sim'].tolist())"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.13 (default, Mar 28 2022, 06:59:08) [MSC v.1916 64 bit (AMD64)]"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "5fe10bf018ef3e697f9035d60bf60847932a12bface18908407fd371fe880db9"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
